package pentago.engine;

import java.awt.Point;

import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;

import pentago.exceptions.PentagoInvalidMarblePositionException;
import pentago.exceptions.PentagoInvalidMoveException;
import pentago.exceptions.PentagoInvalidPlatePositionException;
import pentago.exceptions.PentagoInvalidRotationException;
import pentago.exceptions.PentagoUnknownPlayerException;

public class GameBoard {

    public static final byte UNKNOWN_POSITION = -1;
    public static final byte UPPER_LEFT = 0;
    public static final byte UPPER_RIGHT = 1;
    public static final byte LOWER_LEFT = 2;
    public static final byte LOWER_RIGHT = 3;

    private static GameBoard gameBoard = null;

    public static int LEFT_PLATE_SPACING = 20;
    public static int TOP_PLATE_SPACING = 21;
    public static int MARBLE_RIGHT_PIXEL_SHIFT = 38 + 200; // Spacing between
							   // marbles +
    // marble size
    public static int MARBLE_DOWN_PIXEL_SHIFT = 38 + 200;

    private int plateWidth;
    private int plateHeight;

    private byte[][] boardState;

    private Image plate;

    private Image marble;
    private double scale;

    public static GameBoard getInstance(Image platePath, Image marblePath,
	    float scale, byte[][] boardState) throws SlickException {
	if (gameBoard == null)
	    gameBoard = new GameBoard(platePath, marblePath, scale, boardState);
	return gameBoard;
    }

    public static GameBoard getInstance() {
	if (gameBoard == null)
	    throw new NullPointerException();
	return gameBoard;
    }

    private GameBoard(Image plateImage, Image marbleImage, float scale,
	    byte[][] boardState) throws SlickException {
	this.plate = new Image(plateImage.getTexture());
	this.marble = new Image(marbleImage.getTexture());
	this.scale = scale;
	LEFT_PLATE_SPACING *= scale;
	TOP_PLATE_SPACING *= scale;
	MARBLE_RIGHT_PIXEL_SHIFT *= scale;
	MARBLE_DOWN_PIXEL_SHIFT *= scale;
	this.plateWidth = (int) (plate.getWidth() * scale);
	this.plateHeight = (int) (plate.getHeight() * scale);
	this.boardState = boardState.clone();
    }

    public Image getPlateImage() {
	return plate;
    }

    public Image getMarbleImage() {
	return marble;
    }

    public byte[][] getBoardState() {
	return boardState.clone();
    }

    public void addMarble(Point marblePosition, byte player)
	    throws PentagoInvalidMoveException, PentagoUnknownPlayerException {
	int x = marblePosition.x;
	int y = marblePosition.y;
	if (boardState[x][y] != GameEngine.NO_PLAYER)
	    throw new PentagoInvalidMoveException();
	if (player != GameEngine.FIRST_PLAYER
		&& player != GameEngine.SECOND_PLAYER)
	    throw new PentagoUnknownPlayerException();

	boardState[x][y] = player;
    }

    public void removeMarble(Point marblePosition)
	    throws PentagoInvalidMoveException {
	int x = marblePosition.x;
	int y = marblePosition.y;
	if (boardState[x][y] == GameEngine.NO_PLAYER)
	    throw new PentagoInvalidMoveException();

	boardState[x][y] = GameEngine.NO_PLAYER;
    }

    public Point getPlatePixelCoordinates(byte platePosition)
	    throws PentagoInvalidPlatePositionException {
	int xPos = 0;
	int yPos = 0;

	switch (platePosition) {
	case UPPER_LEFT:
	    return new Point(xPos, yPos);
	case UPPER_RIGHT:
	    xPos = (int) (plate.getWidth() * scale);
	    return new Point(xPos, yPos);
	case LOWER_LEFT:
	    yPos = (int) (plate.getHeight() * scale);
	    return new Point(xPos, yPos);
	case LOWER_RIGHT:
	    xPos = (int) (plate.getWidth() * scale);
	    yPos = (int) (plate.getHeight() * scale);
	    return new Point(xPos, yPos);
	default:
	    throw new PentagoInvalidPlatePositionException();
	}
    }

    public Point getMarblePixelCoordinates(Point marblePosition)
	    throws PentagoInvalidMarblePositionException {

	byte platePosition = getPlateFromMarbleTablePosition(marblePosition);

	int marbleRightPlateShift = (int) (marblePosition.getX() % 3);
	int marbleDownPlateShift = (int) (marblePosition.getY() % 3);

	int xPos = LEFT_PLATE_SPACING;
	int yPos = TOP_PLATE_SPACING;
	xPos += marbleRightPlateShift * MARBLE_RIGHT_PIXEL_SHIFT;
	yPos += marbleDownPlateShift * MARBLE_DOWN_PIXEL_SHIFT;

	switch (platePosition) {
	case UPPER_LEFT:
	    return new Point(xPos, yPos);
	case UPPER_RIGHT:
	    xPos += plateWidth;
	    return new Point(xPos, yPos);
	case LOWER_LEFT:
	    yPos += plateHeight;
	    return new Point(xPos, yPos);
	default:
	    xPos += plateWidth;
	    yPos += plateHeight;
	    return new Point(xPos, yPos);
	}
    }

    public byte getPlateFromMarbleTablePosition(Point marblePosition)
	    throws PentagoInvalidMarblePositionException {
	if (!isMarblePositionValid(marblePosition))
	    throw new PentagoInvalidMarblePositionException();

	if (marblePosition.x < 3 && marblePosition.y < 3)
	    return UPPER_LEFT;
	else if (marblePosition.x >= 3 && marblePosition.y < 3)
	    return UPPER_RIGHT;
	else if (marblePosition.x < 3 && marblePosition.y >= 3)
	    return LOWER_LEFT;
	else
	    return LOWER_RIGHT;
    }

    public void rotatePlate(byte platePosition, byte direction)
	    throws PentagoInvalidRotationException,
	    PentagoInvalidPlatePositionException {

	if (direction != Move.CLOCKWISE && direction != Move.COUNTERCLOCKWISE)
	    throw new PentagoInvalidRotationException();
	if (!isPlatePositionValid(platePosition))
	    throw new PentagoInvalidPlatePositionException();

	byte[][] singlePlate = new byte[3][3];

	int shiftRight = 0;
	int shiftDown = 0;

	switch (platePosition) {
	case UPPER_RIGHT:
	    shiftRight = 3;
	    break;
	case LOWER_LEFT:
	    shiftDown = 3;
	    break;
	case LOWER_RIGHT:
	    shiftRight = 3;
	    shiftDown = 3;
	    break;
	}

	if (direction == Move.CLOCKWISE) {
	    for (int x = 0; x < 3; x++)
		for (int y = 0; y < 3; y++) {
		    singlePlate[x][y] = boardState[y + shiftRight][2 - x
			    + shiftDown];
		}
	} else {
	    for (int x = 0; x < 3; x++)
		for (int y = 0; y < 3; y++) {
		    singlePlate[x][y] = boardState[2 - y + shiftRight][x
			    + shiftDown];
		}
	}

	for (int x = 0; x < 3; x++)
	    for (int y = 0; y < 3; y++)
		boardState[x + shiftRight][y + shiftDown] = singlePlate[x][y];
    }

    public boolean positionEmpty(Point position) {
	return boardState[position.x][position.y] == GameEngine.NO_PLAYER;
    }

    private boolean isMarblePositionValid(Point marbleCoordinates) {
	return (marbleCoordinates.x >= 0 && marbleCoordinates.x <= 5
		&& marbleCoordinates.y >= 0 && marbleCoordinates.y <= 5);
    }

    private boolean isPlatePositionValid(byte position) {
	return (position == UPPER_LEFT || position == UPPER_RIGHT
		|| position == LOWER_LEFT || position == LOWER_RIGHT);
    }

}
